Skip to content

Build the FIPS profile in CI, and make it build again - #1009

Open
fredericgermain wants to merge 4 commits into
netty:mainfrom
fredericgermain:dev-images-patchelf
Open

fredericgermain wants to merge 4 commits into
netty:mainfrom
fredericgermain:dev-images-patchelf

Conversation

@fredericgermain

@fredericgermain fredericgermain commented Sep 9, 2026 •

Copy link
Copy Markdown
Contributor

Follow-up to #997 and #1008.

Motivation

#1008 showed the blind spot: nothing in CI builds the FIPS profile, so a change to the release profiles that is not ported to it only fails on someone's downstream build. Building it in CI turned up more: the profile no longer builds on a fresh machine at all (the FIPS tarball bucket is private now) and does not link on aarch64.

Changes, one commit each

  1. patchelf in the Arch and openSUSE dev images. Their build service has failed since Make the Linux artifacts loadable on musl (Alpine) again #997.
  2. FIPS profile. BoringSSL is cloned from git at a pinned sha, the head of fips-20260721, Google's newest FIPS branch (the tarball at commondatastorage.googleapis.com/chromium-boringssl-fips returns 403 now); CMAKE_POSITION_INDEPENDENT_CODE is back (dropped in Upgrade to BoringSSL 6d503ae1 #950, breaks the aarch64 link); ninja builds only crypto ssl decrepit bssl; the compiler is a property (fipsCC/fipsCXX, default clang).
  3. musl on modern toolchains. Built on a current distro the artifact imports a few glibc-only symbols from the prebuilt static libstdc++/libgcc and glibc 2.38's strtol redirects: it loads on Alpine but ldd fails. -Wl,--gc-sections on the FIPS link line and five weak fallbacks in musl_compat.c, measured with an empty file. No-op on the CentOS release images.
  4. CI. A Debian 13 image with every input pinned (image digest, snapshot.debian.org timestamp, clang version, Go version and checksum), legs debian13-x86_64 and debian13-x86_64-fips, and bare-Alpine musl-verify legs on both jars. The FIPS one matters most: the module's integrity check runs in an ELF constructor at dlopen, so only a real load proves the post-link patchelf left it intact.

Verification

All checks pass on this head. Both profiles were also built on amd64 outside CI: zero musl-check warnings, both jars load on bare Alpine, the FIPS one also with gcompat.

@fredericgermain

Copy link
Copy Markdown
Contributor Author

Still WIP, the last validation run is in progress. I'll come back at it tonight. Not critical for a release IMHO, but nice to have a better support for FIPS in latest release

  • A few non-critical fixes on top of this: the aarch64 link needs CMAKE_POSITION_INDEPENDENT_CODE back (dropped in Upgrade to BoringSSL 6d503ae1 #950), and a bare ninja builds all 564 BoringSSL targets where four are used.
  • The FIPS source tarballs on commondatastorage.googleapis.com are down (403 on every object, 401 on the bucket), so the profile now clones the git repo at the pinned sha, which is what FIPS.md recommends anyway. Downstream builds only kept working off the download plugin's cache.
  • Reverting the pin to the 2024-08-05 module (fips-20240805), the one that actually holds a certificate (#5244, issued April 2026); fips-20251031 is still in the CMVP queue. Same idea for the toolchain: clang 17.0.6, go 1.22.3, ninja 1.12.1, cmake 3.29.3 as named in that certificate's security policy, instead of clang-12. Any newer fips-YYYYMMDD head still builds with -DboringsslBranch=. Three newer SSL_CREDENTIAL setters had to be gated on BORINGSSL_API_VERSION for that module.
  • CI builds it on Debian 13 rather than Ubuntu: its archive carries exactly that clang and ninja, and it matches the other dev images.
  • Built on a toolchain that modern, the artifact picks up glibc-only imports (fortify, the glibc 2.38 _isoc23* redirects, and friends) that make ldd fail on Alpine even though it loads. Handled in musl_compat.c; it applies to anyone building this profile on a current Ubuntu or Debian.

For discussion: a certified pin means shipping two-year-old crypto source, which contradicts normal patch hygiene. The 2025 modules sit at "Comment Resolution" on NIST's in-process list 10 to 20 months after their version dates, and the CMVP's stated goal of a six-month queue by end of 2025 and none by mid-2026 (OpenSSL Conference, Oct 2025) is not happening. FIPS.md now documents an update stream, main with -DFIPS=1, per the FedRAMP policy.
There are also some 2026 fips tag in the git repo, but it feels they didn't bother to submit them yet.

@normanmaurer

Copy link
Copy Markdown
Member

@fredericgermain let me know once this is ready

@fredericgermain

Copy link
Copy Markdown
Contributor Author

Hi @normanmaurer,

Let me try to finish that this week.

Motivation:

netty#997 made the boringssl-static native-jar step run `patchelf --remove-needed`
on Linux, but only the two CI images got the binary. The `build` service of
the Arch and openSUSE images has failed since with "patchelf: not found". CI
does not run them, so nothing flagged it.

Modifications:

Add the distro's `patchelf` package to both images.

The Debian 7 image is left alone: wheezy packages no patchelf, and its GCC 4.9
cannot build BoringSSL's C++17 anyway, so only its dynamic-only service, the
one CI runs, is usable.

Result:

`docker compose ... run build` works again on Arch and openSUSE.
Motivation:

The fips-boringssl-static profile no longer builds from a clean checkout:

- Its source tarball on commondatastorage.googleapis.com/chromium-boringssl-fips
  is no longer public (403). Downstream builds only work off
  download-maven-plugin's cache.
- netty#950 dropped CMAKE_POSITION_INDEPENDENT_CODE. BoringSSL only sets it for the
  bcm targets, so the aarch64 link fails: "relocation
  R_AARCH64_ADR_PREL_PG_HI21 ... recompile with -fPIC".

It also runs a bare `ninja`, which builds all 564 BoringSSL targets, mostly
tests nobody runs, and it hardcodes clang-12.

Modifications:

- Clone BoringSSL with maven-scm and check out a pinned sha, as the default
  profile does. The pin is the head of fips-20260721, Google's newest FIPS
  branch (see FIPS.md). The sha goes into the jar manifest.
- -DCMAKE_POSITION_INDEPENDENT_CODE=TRUE, as the default profile passes.
- ninja builds crypto, ssl, decrepit and bssl only.
- fipsCC / fipsCXX properties, default `clang` / `clang++`.

Result:

The profile builds from a clean checkout, in minutes. The module it contains
holds no certificate yet.
Motivation:

Built on a current distro (Debian 13 here) instead of the CentOS release
images, the artifact imports glibc-only symbols. It still loads on Alpine
under the JVM, which binds lazily, but `ldd`, LD_BIND_NOW and
docker/musl-verify fail. Building with an empty musl_compat.c and asking
Alpine's loader what is missing gives six symbols:

- __isoc23_strtol (APR), __isoc23_strtoul (static libstdc++),
  __isoc23_strtoull (BoringSSL): glibc 2.38 redirects strtol and friends
  there under _GNU_SOURCE, and no -D switches it off.
- __libc_single_threaded: static libstdc++, glibc 2.32+.
- _dl_find_object: libgcc_eh.a from gcc 12, glibc 2.35+.
- arc4random: libstdc++'s std::random_device, glibc 2.36+, dragged in by the
  FIPS profile's newer BoringSSL. Nothing calls it.

Modifications:

- FIPS link line: -Wl,--gc-sections. The linker drops the unreachable
  random_device code and the arc4random import with it; the library gets
  117 KB smaller and the FIPS integrity check still passes.
- Weak fallbacks in musl_compat.c for the other five, which sit in live code.
  The strto* ones reach the plain symbols through asm labels, since a literal
  strtol() there would be redirected too and recurse on musl. `__restrict`,
  not `restrict`: Debian 7's GCC 4.9 is gnu90.
- scripts/check_musl_compat.sh asserts the fallbacks are defined, and knows
  the __isoc99_* scanf names musl does export.

Result:

Artifacts built on Debian 13 resolve completely on Alpine. No-op on the CentOS
release images, whose glibc predates all of these.
@fredericgermain

Copy link
Copy Markdown
Contributor Author

@normanmaurer this is ready for review.

  • Rebased on current main. The Debian 13 compose file follows the ghcr.io build-cache convention from Cache docker build images in ghcr.io to speed up CI #1015 (tag debian13).
  • Verified on amd64 for both the normal boringssl-static build and the FIPS-mode one: zero musl-check warnings, and both jars load on bare Alpine, the FIPS one also with gcompat.
  • Reference for the FIPS side: Google's FIPS.md.

It took a few turns to get here, so here is the path, in case the end result looks arbitrary:

  1. First I tried to support the validated 2024 module (fips-20240805, certificate #5244), with the exact toolchain from its security policy. It builds, but tcnative calls three SSL_CREDENTIAL functions that do not exist in that two-year-old tree, so they had to be compiled out behind BORINGSSL_API_VERSION checks. Carrying compatibility patches for an old BoringSSL is not a direction I think we want, so I scrapped it.
  2. Then I followed FIPS.md literally: build main with -DFIPS=1 and the latest stable tools. A floating branch plus floating tools is too many moving parts for a maintainer to support. I have not bought into all of these security practices myself, and I did not want to hand you a CI leg that breaks whenever Google moves.
  3. So everything is pinned, while still following FIPS.md in spirit:
    • BoringSSL is pinned to the head of Google's newest FIPS branch, fips-20260721. It is from July, recent enough. If Google still cuts these branches, presumably they are meant to be used. It may deserve a chat with their team about the current workflow: I do not think FIPS.md is up to date, its link to the FedRAMP policy is dead.
    • The CI image is Debian stable (13), pinned by image digest and by a snapshot.debian.org timestamp, so CI gets no surprises.
    • Go is the one tool that cannot come from Debian: that BoringSSL branch needs Go 1.25.8 and Debian ships 1.24. It is downloaded at a pinned version with a checksum. One more moving part, unfortunately.

On musl: built on a modern distro the artifact needs a few glibc-only fallbacks in musl_compat.c, which I kept to the measured minimum; the CentOS release images are unaffected. I also looked at linking libstdc++ dynamically; static, like the other targets, looks better.

That FIPS build is quite close to something I would consider taking to production. The one thing I am not sure about is whether the compilation differences (clang, FIPS mode, the delocated module) have a bad impact on performance. I plan to run some A/B benchmarks against the normal build, hopefully I find the time.

Two things for discussion:

  1. How official this looks. The -fips artifactId and the BoringSSL-FIPS-Compliant: true manifest entry (Change the artifactId when building FIPS-compliant netty-tcnative-boringssl-static #945) can read as if Netty as a whole provides FIPS compliance, and a CI job may make it look more official still. What the profile produces is a FIPS-mode build that contains BoringCrypto; the binary holds no certificate. FedRAMP's own policy asks vendors not to use terms like "FIPS compliant". I left the name alone because it changes what downstream builders consume. Let me know in review if I should suffix it with -boringcrypto instead, or something else.
  2. I would recommend simplifying the CI. The -fedora variant has no purpose any more: it existed for the OpenSSL 1.0 soname difference, and now that everything links libssl.so.3 the two Linux artifacts are the same library built on two EOL images. I would probably recommend Debian 13, or another modern distro, as the base builder, with a crosstool-ng toolchain if we want to keep the glibc floor low. Happy to open a separate issue with the details.

The commits are self-contained if you would rather take the Arch and openSUSE patchelf fix separately.

@fredericgermain
fredericgermain marked this pull request as ready for review September 23, 2026 07:38
Comment thread boringssl-static/pom.xml
</execution>
</executions>
<configuration>
<url>https://commondatastorage.googleapis.com/chromium-boringssl-fips/boringssl-${boringsslBranch}.tar.xz</url>

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's the tar bundle that's actually FIPS validated, isn't it? I'm not sure we can just jump to the latest commit on the boringssl fips branch.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

From https://boringssl.googlesource.com/boringssl/+/refs/heads/main/crypto/fipsmodule/FIPS.md
The last validated module is 2024-08-05 (fips-20240805), certificate (#5244, issued April 2026)

It was updated already to fips-20251031 previously in netty-tcnative. I believe this had more to do to get newer API and avoid compilation problem. So the current version is not fips validated.

FIPS.md recommand to use main directly.

Also to be noted, https://commondatastorage.googleapis.com/chromium-boringssl-fips is not working anymore, we need to fetch git commits directly on the upstream repo.

Comment thread docker/Dockerfile.debian13 Outdated
&& tar -C /opt -xzf go$go_version.linux-$ARCH.tar.gz && rm go$go_version.linux-$ARCH.tar.gz \
&& ln -s /opt/go/bin/go /usr/local/bin/go && ln -s /opt/go/bin/gofmt /usr/local/bin/gofmt \
&& ln -s /usr/lib/jvm/java-21-openjdk-$ARCH /usr/lib/jvm/java-21 \
&& go version && clang --version | head -1 && ninja --version && cmake --version | head -1

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this runs in a sub-shell that doesn't have pipefail, so the exit code of e.g. clang --version | head -1 will actually be from head -1 instead of clang --version.

I think it's harmless to just drop the | head -1 part.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right, the pipe is hiding the exit status of clang --version and cmake --version. Dropped both | head -1.

Comment thread docker/Dockerfile.debian13 Outdated
# Not pinned to linux/amd64 like the older images, so it can also be built natively on an
# arm64 host for a quick local run. CI builds it on amd64 runners.
#
# Unlike the CentOS 6 image this is NOT a release builder: its artifact has a glibc 2.34 floor.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

glibc 2.34 floor

In the readme you say 2.41?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The README was wrong. 2.41 is the image's own glibc, the artifact floor is 2.34.

Fixed.

Well done on catching this.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Dropped the number from the doc actually, we probably don't need this in a README. It now only says the image is not a release builder because it'd require a newer glibc.

…both jars on Alpine

Motivation:

CI builds boringssl-static only on the two release images and never builds the
FIPS profile. netty#997 could add the musl check to that profile without the
matching link changes, and nothing failed until a downstream build did (netty#1008).

Modifications:

- docker/Dockerfile.debian13 + docker-compose.debian-13.yaml, services `build`
  and `build-fips`. Every input is pinned: base image by digest, Debian
  archive by snapshot.debian.org timestamp, clang by version, Go by version
  and checksum (BoringSSL's go.mod is ahead of trixie's Go). trixie has no
  JDK 8, so the build runs on JDK 21 with the pom's --release 8.
- ci-pr.yml / ci-build.yml: legs debian13-x86_64 and debian13-x86_64-fips,
  plus bare-Alpine musl-verify legs on their jars. The FIPS module's integrity
  check runs in an ELF constructor at dlopen, so only a real load proves the
  post-link patchelf left it intact.

Result:

A change to the FIPS profile, or one to the release profiles that is not
ported to it, fails on the PR. Not a release image: its artifacts need a
newer glibc than the release ones.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants